<?php

namespace framework\nosql;

use Psr\SimpleCache\CacheInterface;

/**
 *
 * APCu - APC User Cache
 *
 * https://pecl.php.net/package/APCu
 *
 * 

  ; APCU 配置
  ; [apcu]
  extension = "apcu.so"
  apc.enabled=1
  apc.shm_size=256M
  ;apc.enable_cli = 1
  apc.shm_segments=4
  apc.gc_ttl=3600
  apc.gc_maxlifetime=86400
  apc.num_slots=200000
  apc.serializer=igbinary
 *
 */
class ApcuCache implements CacheInterface {

    /**
     * @var string
     */
    private $namespace;

    /**
     * @var int
     */
    private $defaultLifetime;

    public function __construct(string $conf_name = 'apcu_cache') {

        $config = \framework\core\Config::getInstance()->get($conf_name);

        $namespace = isset($config['prefix']) ? $config['prefix'] : 'guipin_';
        $this->namespace = $namespace;

        $expire = isset($config['expire']) ? $config['expire'] : 3600;
        $this->defaultLifetime = $expire;
    }

    /**
     * 　单实例化
     * @staticvar array $obj
     * @param type $conf_name
     * @return \self
     */
    public static function getInstance(string $conf_name = 'apcu_cache') {
        static $obj = [];
        if (!isset($obj[$conf_name])) {
            $obj[$conf_name] = new self($conf_name);
        }
        return $obj[$conf_name];
    }

    public function get(string $key, mixed $default = null): mixed {
        $this->assertKeyName($key);
        $key = $this->buildKeyName($key);

        $value = apcu_fetch($key, $success);

        return $success === false ? $default : $value;
    }

    public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool {
        $this->assertKeyName($key);
        $key = $this->buildKeyName($key);

        $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl;

        return apcu_store($key, $value, (int) $ttl);
    }

    public function delete(string $key): bool {
        $this->assertKeyName($key);
        $key = $this->buildKeyName($key);

        return apcu_delete($key);
    }

    public function clear(): bool {
        return apcu_clear_cache();
    }

    public function getMultiple($keys, $default = null): array {
        $this->assertKeyNames($keys);
        $keys = $this->buildKeyNames($keys);

        $result = apcu_fetch($keys);

        if (!is_null($default) && is_array($result) && count($keys) > count($result)) {
            $notFoundKeys = array_diff($keys, array_keys($result));
            $result = array_merge($result, array_fill_keys($notFoundKeys, $default));
        }

        $mappedResult = [];

        foreach ($result as $key => $value) {
            $key = preg_replace("/^$this->namespace/", '', $key);

            $mappedResult[$key] = $value;
        }

        return $mappedResult;
    }

    public function setMultiple($values, $ttl = null): bool {
        $this->assertKeyNames(array_keys($values));

        $mappedByNamespaceValues = array();

        foreach ($values as $key => $value) {
            $mappedByNamespaceValues[$this->buildKeyName($key)] = $value;
        }

        $ttl = is_null($ttl) ? $this->defaultLifetime : $ttl;

        $result = apcu_store($mappedByNamespaceValues, (int) $ttl);

        return $result === true ? true : (is_array($result) && count($result) == 0 ? true : false);
    }

    public function deleteMultiple($keys): bool {
        $this->assertKeyNames($keys);
        $keys = $this->buildKeyNames($keys);

        $result = apcu_delete($keys);

        return count($result) === count($keys) ? false : true;
    }

    public function has(string $key): bool {
        $this->assertKeyName($key);
        $key = $this->buildKeyName($key);

        return (bool) apcu_exists($key);
    }

    public function inc(string $key, int $step = 1, int $ttl = 0) {

        $this->assertKeyName($key);
        $key = $this->buildKeyName($key);

        if (apcu_exists($key)) {
            return apcu_inc($key, $step, $success, $ttl);
        }

        return apcu_store($key, $step, $ttl);
    }

    public function dec(string $key, int $step = 1, int $ttl = 0) {

        $this->assertKeyName($key);
        $key = $this->buildKeyName($key);

        if (apcu_exists($key)) {
            return apcu_dec($key, $step, $success, $ttl);
        }

        return apcu_store($key, -$step, $ttl);
    }

    public function info() {
        return apcu_cache_info();
    }

    /**
     * @param string $key
     *
     * @return string
     */
    private function buildKeyName($key) {
        return $this->namespace . $key;
    }

    /**
     * @param string[] $keys
     *
     * @return string[]
     */
    private function buildKeyNames(array $keys) {
        return array_map(function ($key) {
            return $this->buildKeyName($key);
        }, $keys);
    }

    /**
     * @param mixed $key
     *
     * @throws InvalidArgumentException
     */
    private function assertKeyName($key) {
        if (!is_scalar($key) || is_bool($key)) {
            throw new InvalidArgumentException();
        }
    }

    /**
     * @param string[] $keys
     *
     * @throws InvalidArgumentException
     */
    private function assertKeyNames(array $keys) {
        array_map(function ($value) {
            $this->assertKeyName($value);
        }, $keys);
    }
}
